昨天整理了 Discord 的角色卡片、進度與操作提示。正常執行時,使用者可以看出目前輪到哪位 Agent,以及接下來要做什麼。
但流程中斷時,光顯示「發生錯誤」還不夠。模型逾時後要重新開始嗎?畫面沒出現結果,資料到底有沒有保存?按鈕送出失敗,再按一次會不會重複批准?
今天集中整理錯誤處理,讓 Bot 能說清楚保存狀態與接續方式。Discord 實測也發現三個問題:錯誤卡變成公開訊息、第二次查詢一直思考中,以及一筆舊資料讓整份專案清單失敗。這三項都已修正並重測通過。
這個專案會經過 Discord、Ollama 和 MySQL。錯誤可能發生在不同地方,處理方式也不同。
例如模型尚未下載,重送同一個 Prompt 沒有幫助;資料庫已提交卻沒收到回覆,直接重送建立操作又可能產生重複資料。
因此把分類放進 core/errors.py,用固定的 ErrorCode 對應原因、下一步與恢復策略。下面節錄模型缺失的設定:
ErrorCode.OLLAMA_MODEL_MISSING: (
'設定的模型尚未可用。',
'請管理員確認模型名稱與安裝,再重試原指令。',
RetryPolicy.AFTER_FIX,
),
程式依例外型別、明確代碼與原始原因分類,不靠中文錯誤文字猜測。指令、自然語言入口與決策按鈕再交給共用的 ErrorHandler,產生一致的錯誤卡。
卡片包含「發生什麼」、「目前保存狀態」、「下一步」與「查詢代碼」。使用者可以先照提示操作;需要管理員協助時,再提供查詢代碼對照日誌。
底層例外不會直接貼到 Discord。即使模型回覆或資料庫錯誤包含額外內容,對外仍使用固定說明。

Ollama 錯誤現在會區分連線失敗、逾時、模型缺失,以及回覆格式或內容沒有通過驗證。
會議管理器負責有限重試,預設最多兩次總嘗試,也就是第一次呼叫加一次重試。缺少模型這類必須先修正設定的錯誤,不會一直自動重跑。
成功且已保存的 Agent 步驟會保留在檢查點。討論中斷後,先用 /status 確認階段,再用 /resume 接續;草案或審查則依提示使用 /draft、/review。
這裡也分開「模型沒有完成」和「模型已完成,但顯示失敗」。如果結果已保存,只是 Discord 卡片沒有送出,程式會留下顯示失敗紀錄,不會把成功步驟改成失敗,也不會為了補畫面再呼叫一次模型。
流程結束或中斷時,仍要清除執行中的工作並釋放伺服器鎖。否則前一次出錯後,後續操作可能一直被判定為忙碌。
MySQL 的等待也加上期限:預設取得連線最多等 10 秒,SQL、讀取結果與交易操作各有 30 秒的等待上限。
但加上 timeout 並不代表逾時後就能安全重送。資料庫可能已完成 commit,只是回覆在傳回途中遺失。因此錯誤處理會保留交易進行到哪一步:
| 保存狀態 | 接下來的處理 |
|---|---|
| 尚未開始寫入 | 修正問題後再操作 |
| 已回滾 | 確認目前進度,再重試原操作 |
| 已提交 | 查詢已保存結果,不重做已完成操作 |
| 結果不明 | 先查證,不能直接宣稱沒有保存 |
唯讀查詢遇到暫時錯誤可以有限重試,寫入交易則不自動重播。批准方案原本的原子交易與版本檢查也保留。
自然語言建案另外加入 write_outcome_unknown 狀態。建立結果不明時,暫時擋住再次「確認」,也不能用「取消」清掉保護後重新建立。
使用者回覆「查證」,程式會查同一個伺服器、同一個實際生成的專案 ID。找到保存紀錄後才提供接續方式,不會拿相似標題猜是哪一筆。
這個保護狀態仍放在記憶體中,Bot 重啟後不會恢復未提交的收集對話。所以重啟後如果不確定先前是否建案成功,應先用 /projects 查看,不能直接假設草稿消失就代表資料沒寫入。
自動測試可以檢查程式邏輯,但替身物件不一定完整重現 Discord 的行為。今天的實測就找出三個正常流程測試沒有攔住的問題。
輸入不存在的專案 ID 執行 /start,Bot 正確辨識「找不到專案」,但錯誤卡出現在公開頻道。
原本程式先用公開 defer 告訴 Discord「正在處理」,出錯後才傳送 ephemeral=True 的 followup。ephemeral 代表只有操作的人看得到,卻不能在這個時點改變原等待訊息的私密狀態。
Discord 官方文件說明,緊接 deferred response 的第一則 followup 可能是完成原回覆,會保留初始 defer 的設定。官方 Followup 說明
現在 safe_error_reply() 會先確認原回覆是否仍是公開的等待訊息。如果是,就把它完成為中性通知,再另外傳送私人錯誤卡。公開通知只說操作未完成,沒有錯誤細節。
若原本已是私人等待訊息,就不額外增加公開通知;如果公開原回覆已完成,也不覆寫成功結果。私人錯誤傳送失敗時,則停止回覆,不改貼到公開頻道。
重測後,錯誤卡已顯示「只有您才能看到這個」,公開頻道只留下中性通知。這次沒有觀察到 Token 或密碼外洩,但原本的可見範圍仍然需要修正。

第一次查詢正常,第二次卻一直顯示「正在思考」。這次不是 Qwen 回得太慢,因為 /status 本來就不需要呼叫模型。
原因是程式找到上一次的狀態卡後,只更新舊訊息就結束,沒有完成這次新指令的回覆。每次 Slash 指令都有自己的 Interaction,編輯上一張卡片不等於回覆這一次操作。
現在移除跨 Interaction 的狀態卡快取,每次都完成自己的私人回覆。以下是產生頁面後的程式片段:
pages = build_status_embeds(
record, is_running=running, event=active[1] if active else None
)
for page in pages:
await safe_followup_send(interaction, embed=page, ephemeral=True)
連續兩次 /status 的重測都正常,第二次不再停在等待畫面,也沒有新增公開狀態卡。這裡調整的是私人查詢;DAY 26 用來追蹤長流程的頻道進度卡,仍可更新同一則訊息。
第三個問題來自一筆缺少驗收條件的舊專案。轉成 Project 物件時,資料驗證失敗,連帶讓其餘正常專案也無法顯示。
錯誤又被歸成 INVALID_ARGUMENT,看起來像使用者把指令打錯,實際上需要修復的是已保存資料。
現在清單查詢會隔離異常紀錄,回傳有效專案與異常筆數。_build_projects() 的處理片段如下:
try:
projects.append(Project.from_dict(data))
except ProjectDataError as error:
if invalid_rows is not None:
invalid_rows.append(project_id)
continue
raise ProjectRepositoryError(
"MySQL 專案資料格式不正確。",
code=ErrorCode.PROJECT_DATA_INVALID,
write_outcome='not_started',
) from error
只有清單查詢允許略過異常資料。單筆讀取與啟動仍須通過完整驗證,而且啟動前就先檢查,避免不完整專案占用伺服器的進行中名額。
重測時,/projects 顯示 20 筆有效專案,另提示 1 筆資料待修復。那筆舊紀錄仍保留,沒有刪除,也沒有自行補造驗收條件。

錯誤集中處理後,也整理了終端機日誌。管理員需要知道哪個階段失敗,但不需要在每次例外時印出完整 Prompt、模型回答或 SQL 參數。
core/logging_security.py 記錄固定事件、錯誤代碼、查詢代碼、例外型別與必要識別資料。憑證遮罩則補在輸出端,處理已登記的秘密值,以及常見的 Bearer、password、token 等格式。
主要做法仍是少記錄內容。遮罩不能辨識所有自由文字,因此不能把「有打碼」當成可以隨意記錄需求與回答的理由。
這些測試涵蓋模型失敗後接續、提交結果不明時禁止重送,以及錯誤回覆不洩漏原始內容。三個實機問題也都回到 Discord 重測,不能只看程式改完就算完成。
真實模型 qwen3.5:9b 的一般連線已確認能回覆;Ollama 與 MySQL 停服恢復、權限撤除、跨帳號及重啟後按鈕等故障情境,仍未全部完成實機驗收。
把中斷後的操作整理得更清楚,也補上這次實測找到的三個漏洞。之後再遇到錯誤,使用者至少能先確認保存狀態,知道該接續、查證,還是請管理員修正設定。